User:Suffusion of Yellow/batchtest-plus-core.js
Appearance
Code that you insert on this page could contain malicious content capable of compromising your account. If you import a script from another page with "importScript", "mw.loader.load", "iusc", or "lusc", take note that this causes you to dynamically load a remote script, which could be changed by others. Editors are responsible for all edits and actions they perform, including by scripts. User scripts are not centrally supported and may malfunction or become inoperable due to software changes. A guide to help you find broken scripts is available. If you are unsure whether code you are adding to this page is safe, you can ask at the appropriate village pump. This code will be executed when previewing this page. |
This user script seems to have a documentation page at User:Suffusion of Yellow/batchtest-plus-core. |
/*
* Adds a "Test against past hits" button to [[Special:AbuseFilter/test]].
* Useful for testing your changes to a filter without tediously checking
* each old hit with [[Special:AbuseFilter/examine]].
*
* Only the "user", "page", "before", and "after" fields are respected.
*/
// jshint esnext: false, esversion: 8
// <nowiki>
(function() {
/* globals $, mw, OO */
'use strict';
// If forking, PLEASE change this line.
const API_USER_AGENT = "batchtest-plus/0.5 (https://en.wikipedia.org/wiki/User:Suffusion_of_Yellow/batchtest-plus.js)";
const DEFAULT_CONFIG = {
"default" : {
batchSize: 100, // Same as Special:AbuseFilter/test
maxConcurrentRequests: 10, // Too many seems to cause random HTTP timeouts
falsePositveTestFilter: false, // Filter at your wiki matching a random sample of edits
enableFalseNegativeTest: false
},
"en.wikipedia.org" : {
falsePositiveTestFilter: 1201,
enableFalseNegativeTest: true
}
};
let config = { }, api;
function handleApiError(code, details) {
if (typeof code != 'string')
throw code; // Something went very wrong
if (code == "http" && details.textStatus == "abort")
return { aborted: true }; // Aborted by user, not an error
return {
error : (code == "http") ?
"HTTP error: " + details.textStatus :
"API returned error \"" + code + "\": " + details.error.info
};
}
// Make API abuselog entry into something human-readable.
function formatLogEntry(log) {
let link = (target, text) =>
$('<a></a>', {
href: mw.util.getUrl(target),
text: text
});
let sclink = (params, text) =>
$('<a></a>', {
href: new mw.Uri(mw.config.get('wgScript')).extend(params),
text: text
});
let monthNamesShort =
[ "", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec" ];
let t = log.timestamp;
let date = parseInt(t.slice(8, 10)) +
" " + monthNamesShort[parseInt(t.slice(5, 7))] +
" " + t.slice(0, 4);
let time = t.slice(11, 19);
let $li = $('<li></li>').append(
"(",
link("Special:AbuseLog/" + log.id, "details"),
" | ",
link("Special:AbuseFilter/examine/log/" + log.id, "examine"),
" | ",
log.revid ? link("Special:Diff/" + log.revid, "diff") : "diff",
") . . ",
link("Special:AbuseFilter/" + log.filter_id,
log.filter_id).attr("title", log.filter),
" (",
mw.html.escape(log.action),
" -> ",
$('<span></span>', {
text : log.result || "none",
class : "filter-highlighter-" + (log.result || "noaction")
}),
") . . ",
link(log.title, log.title),
" (",
log.title.indexOf("Special:") !== 0 ?
sclink( { title : log.title,
action : "history" }, "hist") : "hist",
" | ",
sclink( { title: "Special:AbuseLog",
wpSearchTitle : log.title }, "log"),
"); ",
mw.html.escape(time),
" . . ",
link("Special:Contributions/" + log.user, log.user),
" (",
link("User talk:" + log.user, "talk"),
" | ",
sclink( { title: "Special:AbuseLog",
wpSearchUser : log.user }, "log"),
")"
);
$li.addClass("btp-maybematch");
return [date, $li];
}
async function doTest(abuselog, filter, stats, cb) {
let pending = [];
for(let log of abuselog) {
let idx = pending.length < config.maxConcurrentRequests ?
pending.length : await Promise.race(pending);
if (idx === undefined)
break; // Something went wrong with the last request
pending[idx] = api.post({
action : 'abusefiltercheckmatch',
filter : filter,
logid : log.id
}).catch(handleApiError)
.then(response => {
let result;
if (response.aborted)
return;
stats.tested++;
if (!response || !response.abusefiltercheckmatch) {
stats.errors++;
result = null;
} else {
result = response.abusefiltercheckmatch.result;
}
if (result)
stats.matches++;
if (cb)
cb(log.id, result, response.error);
return idx;
});
}
await Promise.all(pending);
return stats;
}
async function testAtTestPage(filters, query, testFilter, action, stats) {
if (!query) {
// We got here because the user clicked "Test".
// If they had clicked "continue", query would be defined.
stats = {
tested : 0,
errors: 0,
matches : 0
};
query = {
action : "query",
list : "abuselog",
aflprop : "ids|filter|user|title|action|result|timestamp|revid",
afllimit : config.batchSize
};
let user = $('[name="wpTestUser"]').val();
let title = $('[name="wpTestPage"]').val();
let after = $('[name="wpTestPeriodStart"]').val();
let before = $('[name="wpTestPeriodEnd"]').val();
testFilter = $('[name="wpFilterRules"]').val();
action = $('[name="wpTestAction"]').val();
if (filters.length)
query.aflfilter = filters;
if (user.length)
query.afluser = user;
if (title.length)
query.afltitle = title;
if (before.length)
query.aflstart = before;
if (after.length)
query.aflend = after;
// Cleanup last run, or the normal /test results
$('.mw-changeslist, .btp-results, .btp-progress').remove();
mw.util.$content.append('<div class="btp-results"></div>');
mw.util.$content.append('<h4 class="btp-progress"></h4>');
}
let response = await api.get(query).catch(handleApiError);
if (!response || !response.query || !response.query.abuselog) {
if (response.error)
mw.notify(response.error);
return;
}
let abuselog = response.query.abuselog
.filter((log) => (action === "0" || log.action.includes(action)));
let $results = $('<div></div>'), $loglines = {};
let prev = $(".btp-results").find('h4').last().text();
let $ul = $('<ul></ul>');
$results.append($ul);
for(let log of abuselog) {
let [date, $li] = formatLogEntry(log);
$loglines[log.id] = $li;
if (date != prev ) {
prev = date;
$ul = $('<ul></ul>');
$results.append($('<h4></h4>', { text: date }), $ul);
}
$ul.append($li);
}
$('.btp-results').append($results);
await doTest(abuselog, testFilter, stats, (id, result, err) => {
$loglines[id].removeClass('btp-maybematch').removeAttr("title");
if (result === null) {
$loglines[id].addClass('btp-error').attr("title", err);
} else if (result === true) {
$loglines[id].addClass('btp-match');
} else {
$loglines[id].addClass('btp-nomatch');
}
});
let $summary = $('<h4></h4>').append(
$('<span></span>', {
text: stats.matches + "/" + stats.tested + " match, " + stats.errors + " error(s)"
})
);
if (response.continue) {
$summary.append(
", ",
$('<a></a>', {
text: "continue?"
}).click(() => {
$summary.remove();
query.aflstart = response.continue.aflstart;
testAtTestPage(filters, query, testFilter, action, stats);
})
);
}
$results.append($summary);
// For popups/markblocked/filter-highlighter/etc.
mw.hook('wikipage.content').fire($results);
}
async function testAtFilterEditor(filterRules, id) {
let stats = {
tested : 0,
errors: 0,
matches : 0
};
let query = {
action : "query",
list : "abuselog",
aflprop : "ids|filter",
afllimit : config.batchSize,
aflfilter : id
};
$('.btp-progress').text("Fetching filter log...");
let response = await api.get(query).catch(handleApiError);
if (!response || !response.query || !response.query.abuselog || !response.query.abuselog.length) {
$('.btp-progress').text("Failed to fetch filter log");
return;
}
await doTest(response.query.abuselog, filterRules, stats, () => {
$('.btp-progress').text(
stats.matches + "/" + stats.tested + " match, "
+ stats.errors + " error(s) (Filter rule: "
+ response.query.abuselog[0].filter + ")"
);
});
}
function setupFilterEditor() {
let $form = $("#mw-abusefilter-editing-form");
let $saveButton = $form.find("input[type=submit]");
let FPButton, FNButton;
if (config.falsePositiveTestFilter) {
FPButton = new OO.ui.ButtonWidget({
label: 'FP check',
title: 'Check for false positives'
}).on("click", async () => {
api.abort();
testAtFilterEditor($("#wpFilterRules").val(), config.falsePositiveTestFilter);
});
}
let id = $form.attr("action") && $form.attr("action").match(/\d+$/);
if (config.enableFalseNegativeTest && id) {
FNButton = new OO.ui.ButtonWidget({
label: 'FN check',
title: 'Check for false negatives'
}).on("click", () => {
api.abort();
testAtFilterEditor($("#wpFilterRules").val(), id[0]);
});
}
$saveButton.parent().after(
FPButton && FPButton.$element,
FNButton && FNButton.$element,
$('<span style="font-size:85%"></span>').html('<a href="https://en.wikipedia.org/wiki/User:Suffusion_of_Yellow/batchtest-plus">What\'s this?')
);
$form.append($('<div class="btp-progress"></div>'));
}
function setupTestPage() {
let filterId = mw.config.get('wgPageName').match(/\/(\d+)$/);
let filters = new OO.ui.TextInputWidget({
placeholder: "Filter IDs (separate with pipes)",
value: filterId && filterId[1]
});
let test = new OO.ui.ButtonWidget({
label: "Test"
}).on("click", () => {
api.abort();
testAtTestPage(filters.getValue());
});
let cancel = new OO.ui.ButtonWidget({
label: "Cancel"
}).on("click", () => {
api.abort();
});
let fieldset = new OO.ui.FieldsetLayout({
label: "Test against past hits"
}).addItems([new OO.ui.HorizontalLayout({
items: [filters, test, cancel]
})]);
$('#wpFilterForm').append(fieldset.$element);
}
function setup() {
Object.assign(config, DEFAULT_CONFIG['default']);
Object.assign(config, DEFAULT_CONFIG[mw.config.get('wgServerName')]);
if(window.batchTestPlusConfig) {
Object.assign(config, window.batchTestPlusConfig['default']);
Object.assign(config, window.batchTestPlusConfig[mw.config.get('wgServerName')]);
}
api = new mw.Api( {
ajax: {
headers: {
'Api-User-Agent' : API_USER_AGENT
}
}
});
if (/\/test(\/\d+)?$/.test(mw.config.get('wgPageName')))
setupTestPage();
else if ($('#mw-abusefilter-editing-form') &&
(config.falsePositveTestFilter || config.enableFalseNegativeCheck));
setupFilterEditor();
}
if (mw.config.get('wgCanonicalSpecialPageName') === 'AbuseFilter') {
$.when($.ready,
mw.loader.load("https://en.wikipedia.org/w/index.php?action=raw&title=User:Suffusion_of_Yellow/batchtest-plus.css&ctype=text/css", "text/css"),
mw.loader.using(
['mediawiki.util',
'mediawiki.api',
'mediawiki.Uri',
'oojs-ui-core'])).then(setup);
}
})();
// </nowiki>